/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny.actions.keys;

import com.softwarecraftsmen.inputStreamReaders.InputStreamReader;
import com.softwarecraftsmen.inputStreamReaders.InputStreamReader.Nothing;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectory.CurrentUserHomeDirectory;
import com.softwarecraftsmen.orogeny.filing.AbsoluteFile;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.BufferedInputStream;
import static java.lang.String.format;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyStore;
import static java.security.KeyStore.getInstance;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import static java.util.Locale.UK;
import java.util.concurrent.Callable;

public class ConvenientKeyStore
{
	@NonNls
	private static final String KeyStoreTypeJKS = "JKS";

	@NonNls
	private static final String CertificateFactoryTypeX509 = "X.509";

	private final KeyStore keyStore;
	private final CertificateFactory x509CertificateFactory;

	public ConvenientKeyStore(final @NotNull AbsoluteFile keyStoreLocation, final @Nullable char[] keyStorePassword)
	{
		try
		{
			keyStore = getInstance(KeyStoreTypeJKS);
		}
		catch (KeyStoreException e)
		{
			throw new IllegalStateException(format(UK, "Could not get essential KeyStore type %1$s", KeyStoreTypeJKS));
		}
		try
		{
			x509CertificateFactory = CertificateFactory.getInstance(CertificateFactoryTypeX509);
		}
		catch (CertificateException e)
		{
			throw new IllegalStateException(format(UK, "Could not get essential CertificateFactory type %1$s", CertificateFactoryTypeX509), e);
		}
		try
		{
			keyStoreLocation.readData(new InputStreamReader<Nothing>()
			{
				@NotNull
				public Nothing read(final int inputBufferSize, final @NotNull BufferedInputStream inputStream) throws IOException
				{
					try
					{
						keyStore.load(inputStream, keyStorePassword);
					}
					catch (NoSuchAlgorithmException e)
					{
						throw new IllegalStateException("Should never happen", e);
					}
					catch (CertificateException e)
					{
						throw new IllegalArgumentException(format(UK, "KeyStore location %1$s is incorrect or password is incorrect", keyStoreLocation), e);
					}
					return Nothing;
				}
			});
		}
		catch(IOException e)
		{
			throw new IllegalStateException(format(UK, "Could not use KeyStore location %1$s. Check location and password", keyStoreLocation), e);
		}
	}

	@NotNull
	public X509Certificate[] getSignerCertificateChain(final @NotNull ConvenientKey convenientKey)
	{
		final @NotNull Certificate[] certificateChain = convenientKey.getCertificateChain(keyStore);
		final X509Certificate signerCertificateChain[] = new X509Certificate[certificateChain.length];
		for (int index = 0; index < certificateChain.length; index++)
		{
			signerCertificateChain[index] = createX509Certificate(certificateChain[index]);
		}
		return signerCertificateChain;
	}

	@NotNull
	public PrivateKey getRsaPrivateKey(final @NotNull ConvenientKey convenientKey)
	{
		final Key publicKey = convenientKey.getPublicKey(keyStore);
		final KeyFactory keyFactory;
		try
		{
			keyFactory = KeyFactory.getInstance(publicKey.getAlgorithm());
		}
		catch (NoSuchAlgorithmException e)
		{
			throw new IllegalStateException("Should never happen", e);
		}
		try
		{
			return keyFactory.generatePrivate(keyFactory.getKeySpec(publicKey, RSAPrivateKeySpec.class));
		}
		catch (InvalidKeySpecException e)
		{
			throw new IllegalStateException("Only RSA keys are currently supported", e);
		}
	}

	private X509Certificate createX509Certificate(final Certificate certificate)
	{
		try
		{
			return (X509Certificate) x509CertificateFactory.generateCertificate(new ByteArrayInputStream(getCertificateEncodingData(certificate)));
		}
		catch (CertificateException e)
		{
			throw new IllegalStateException("Could not create X.509 certificate", e);
		}
	}

	private static byte[] getCertificateEncodingData(final Certificate certificate)
	{
		try
		{
			return certificate.getEncoded();
		}
		catch (CertificateEncodingException e)
		{
			throw new IllegalStateException("Could not get user's certificate encoding data", e);
		}
	}

	@NotNull
	public static SpecifyKeyStoreLocation keyStore(final @NotNull AbsoluteFile keyStoreLocation)
	{
		return new SpecifyKeyStoreLocation(keyStoreLocation);
	}

	@NotNull
	public static final SpecifyKeyStoreLocation UserKeyStore = new SpecifyKeyStoreLocation(CurrentUserHomeDirectory.file(".keystore"));

	public static final class SpecifyKeyStoreLocation
	{
		private final AbsoluteFile keyStoreLocation;

		private SpecifyKeyStoreLocation(final @NotNull  AbsoluteFile keyStoreLocation) {this.keyStoreLocation = keyStoreLocation;}

		@NotNull
		public Callable<ConvenientKeyStore> password(final @Nullable String password)
		{
			return new Callable<ConvenientKeyStore>()
			{
				@NotNull
				public ConvenientKeyStore call() throws Exception
				{
					if (password == null)
					{
						return new ConvenientKeyStore(keyStoreLocation, null);
					}
					return new ConvenientKeyStore(keyStoreLocation, password.toCharArray());
				}
			};
		}
	}
}
